
import java.awt.*;
import java.awt.event.*;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;

import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.*;
import javax.swing.*;

import com.dropbox.client.Authenticator;
import com.dropbox.client.DropboxClient;
import com.dropbox.client.TrustedAuthenticator;

import java.io.*;

/**
 * Our main program
 */
public class Stylepad extends Notepad {

	private static ResourceBundle resources;
	private String path = null;					
	private String fileName = null;
	private int paraNum = -1;					//record the number of paragraphs, usually before an update
	private Paragraph[] paragraphs;				//record the properties for each paragraph

	private static boolean isLoadingNext = false;
	private static boolean isLoadingPrev = false;

	private static JLabel lineLabel;
	//public static JLabel state;
	//public static JProgressBar pb;
	private static final String displayInfo = " Current Line Number : ";
	
	// the unique name for the user, each user should user a different name
	private static final String userName = "Alice"; 	

	private DropboxCommunicator dbComm;
	private static  SmartManager sm;
	
	private MyDocumentListener myListener; 
	private MyAdjustmentListener myAdListener;
	/**
	 * each file name is of the following form:
	 * 		para_paraNum_lineNum_N/(username)L
	 * where paraNum is a float number representing which part this file belongs to,
	 * lineNum is a integer indicates how many lines this file contains,
	 * and L/N indicates whether this file has been locked (L) or not (N),
	 */
	
	static {
		try {
			resources = ResourceBundle.getBundle("resources.Stylepad");
		} catch (MissingResourceException mre) {
			System.err.println("Stylepad.properties not found");
			System.exit(0);
		}
	}

	public Stylepad() {
		super();

		DropboxClient dbClient = initializeDBClient();
		dbComm = new DropboxCommunicator(dbClient, "sandbox", "/testjingmei", "/Backup", this, userName); //u099048h passed editor object & userName
		sm = new SmartManager(this, dbComm);
		//dbComm.sm = sm;
		myListener = new MyDocumentListener();
		myAdListener = new MyAdjustmentListener(0);
		scroller.getVerticalScrollBar().addAdjustmentListener(myAdListener);

		openFile();
	}

	@SuppressWarnings("unchecked")
	private DropboxClient initializeDBClient() {

		System.out.println("Waiting for connecting to Dropbox...");
		
		Map config = null;
		Authenticator auth = null;
		
		try {
			config = TrustedAuthenticator.loadConfig("config/trusted_testing.json");
			TrustedAuthenticator trusted_auth = new TrustedAuthenticator(config);
			String username = (String)config.get("testing_user");
			assert username != null : "You failed to set the testing_user for trusted access.";
			String password = (String)config.get("testing_password");
			assert password != null : "You failed to set the testing_password for trusted access.";
			trusted_auth.retrieveTrustedAccessToken (username, password);
			auth = trusted_auth;

		} catch (Exception e) {
			e.printStackTrace();
			assert false : "Total failure trying to access the trusted authenticator." + e;
		}
      
		assert auth != null : "Auth didn't get configured.";

		System.out.println("Connected to Dropbox!");

		return new DropboxClient(config, auth);
		
		
	}

	public static void main(String[] args) {
		String vers = System.getProperty("java.version");
		if (vers.compareTo("1.1.2") < 0) {
			System.out.println("!!!WARNING: Swing must be run with a " +
			"1.1.2 or higher version VM!!!");
		}
		JFrame frame = new JFrame();
		frame.setTitle(resources.getString("Title") + " - " + userName);
		frame.setBackground(Color.lightGray);
		frame.getContentPane().setLayout(new BorderLayout());
		Stylepad stylepad = new Stylepad();
		frame.getContentPane().add("Center", stylepad);

		lineLabel = new JLabel();
		lineLabel.setText(displayInfo + 0);

	    frame.getContentPane().add("South", lineLabel);

		frame.setJMenuBar(stylepad.createMenubar());
		frame.addWindowListener(new AppCloser(frame));
		frame.pack();
		frame.setSize(650, 800);
		frame.setResizable(false);
		frame.show();

	}
		
	/**
	 * Fetch the list of actions supported by this
	 * editor.  It is implemented to return the list
	 * of actions supported by the superclass
	 * augmented with the actions defined locally.
	 */
	public Action[] getActions() 
	{	
		Action[] sysActions = super.getActions ();
		Action[] defaultActions = {
				new NewAction(),
				new OpenAction(),
				new SaveAction(),
				//new LockAction(),
				//new UnlockAction(),
				//new DownAction(),
				//new UpAction(),
				sysActions[1],
				//new StyledEditorKit.FontFamilyAction("font-family-LucidaSans", 
				//"Lucida Sans"),
				
		};
		Action[] a = TextAction.augmentList (sysActions, defaultActions);
		return a;
	}

	/**
	 * Try and resolve the resource name in the local
	 * resource file, and if not found fall back to
	 * the superclass resource file.
	 */
	protected String getResourceString(String nm) {
		String str;
		try {
			str = this.resources.getString(nm);
		} catch (MissingResourceException mre) {
			str = super.getResourceString(nm);
		}
		return str;
	}

	/**
	 * Create an editor to represent the given document.  
	 */
	protected JTextComponent createEditor() {
		//StyleContext sc = new StyleContext();
		//DefaultStyledDocument doc = new DefaultStyledDocument(sc);
		//initDocument(doc, sc);
		//doc.addDocumentListener(new MyDocumentListener());
		JTextPane p = new JTextPane();
		p.setDragEnabled(true);
		p.addCaretListener(new MyCaretListener());
		//p.getCaret().setBlinkRate(0);

		return p;
	}

	/**
	 * Create a menu for the app.  This is redefined to trap 
	 * a couple of special entries for now.
	 */
	protected JMenu createMenu(String key) {
		if (key.equals("color")) {
			return createColorMenu();
		} 
		return super.createMenu(key);
	}


	// this will soon be replaced
	JMenu createColorMenu() {
		ActionListener a;
		JMenuItem mi;
		JMenu menu = new JMenu(getResourceString("color" + labelSuffix));
		mi = new JMenuItem(resources.getString("Red"));
		mi.setHorizontalTextPosition(JButton.RIGHT);
		mi.setIcon(new ColoredSquare(Color.red));
		a = new StyledEditorKit.ForegroundAction("set-foreground-red", Color.red);
		//a = new ColorAction(se, Color.red);
		mi.addActionListener(a);
		menu.add(mi);
		mi = new JMenuItem(resources.getString("Green"));
		mi.setHorizontalTextPosition(JButton.RIGHT);
		mi.setIcon(new ColoredSquare(Color.green));
		a = new StyledEditorKit.ForegroundAction("set-foreground-green", Color.green);
		//a = new ColorAction(se, Color.green);
		mi.addActionListener(a);
		menu.add(mi);
		mi = new JMenuItem(resources.getString("Blue"));
		mi.setHorizontalTextPosition(JButton.RIGHT);
		mi.setIcon(new ColoredSquare(Color.blue));
		a = new StyledEditorKit.ForegroundAction("set-foreground-blue", Color.blue);
		//a = new ColorAction(se, Color.blue);
		mi.addActionListener(a);
		menu.add(mi);

		return menu;
	}

	void initDocument(DefaultStyledDocument doc, StyleContext sc) {
		//Wonderland w = new Wonderland(doc, sc);
		//HelloWorld h = new HelloWorld(doc, sc);
		//Icon alice = new ImageIcon(resources.getString("aliceGif"));
		//w.loadDocument();
		path = null;
		fileName = null;
		paraNum = 1;
		paragraphs = new Paragraph[paraNum];
		paragraphs[0] = new Paragraph("para_1");
		paragraphs[0].isModified = true; 

	}

	JComboBox createFamilyChoices() {
		JComboBox b = new JComboBox();
		String[] fonts = getToolkit().getFontList();
		for (int i = 0; i < fonts.length; i++) {
			b.addItem(fonts[i]);
		}
		return b;
	}

	/**
	 * save file back to Dropbox,
	 * not in use now, as saving is automatically performed
	 */
	public void saveFile(String dir){
		
		//sm.saveAction(dir);
	}
	
	public void openFile(String dir, String filename){
		System.out.println(dir +" "+filename);
		File d = new File(dir);
		if(!d.isDirectory())
			System.err.println("Should provide the parent directory!");

		File[] files = d.listFiles();
		files = sortFileByName(files);
		paraNum = files.length;
		paragraphs = new Paragraph[paraNum];
		for(int i = 0; i < paraNum; i++)
			paragraphs[i] = new Paragraph(files[i].getName());

		Document doc = new DefaultStyledDocument();

		try{
			for (int k = 0; k < files.length; k++){
				FileInputStream fin = new FileInputStream(files[k]);
				ObjectInputStream istrm = new ObjectInputStream(fin);
				Document tmp = (Document) istrm.readObject();
				tmp.remove(tmp.getLength()-1, 1);
				Element section = tmp.getDefaultRootElement();
				int paraCount = section.getElementCount();
				for (int i = 0; i < paraCount; i++) {
					Element ele = section.getElement(i); // paragraph level
					int para = ele.getElementCount();
					//System.out.println(para);
					for(int j = 0; j < para; j++){
						Element content = ele.getElement(j);
						int rangeStart = content.getStartOffset();
						int rangeEnd = content.getEndOffset();
						AttributeSet as = content.getAttributes();

						doc.insertString(doc.getLength(), tmp.getText(rangeStart, rangeEnd - rangeStart), as);
					}
				}
				fin.close();
				istrm.close();
			}
			doc.remove(doc.getLength()-1, 1);
			if(getEditor().getDocument() != null)
				getEditor().getDocument().removeUndoableEditListener(undoHandler);
			getEditor().setDocument(doc);
			doc.addUndoableEditListener(undoHandler);
			resetUndoManager();
			//doc.addDocumentListener(new MyDocumentListener());
			path = dir;
			fileName = filename;
			Frame frame = getFrame();			
			frame.setTitle(fileName);
			validate();
					
		}catch(Exception ioe){
			ioe.printStackTrace();
		}
	}
	

	/**
	 * read files from Dropbox
	 */
	public void openFile(){
		sm.openAction();			
		
		Document doc = this.getEditor().getDocument();
		doc.addUndoableEditListener(undoHandler);
		resetUndoManager();
		doc.addDocumentListener(myListener);

		//Frame frame = getFrame();
		//frame.setTitle(fileName);
		//validate();
	}
	/**
	 * Trys to read a file which is assumed to be a 
	 * serialization of a document.
	 */
	class OpenAction extends AbstractAction {

		OpenAction() {
			super(openAction);
		}

		public void actionPerformed(ActionEvent e) {
			
			openFile();
/*			
			Frame frame = getFrame();
			if (fileDialog == null) {
				fileDialog = new FileDialog(frame);
			}
			fileDialog.setMode(FileDialog.LOAD);
			fileDialog.show();

			String file = fileDialog.getFile();
			if (file == null) {
				return;
			}
			String directory = fileDialog.getDirectory();
			//obtain the file name, not include the path
			directory = directory.substring(0, directory.length()-1);
			int index = directory.lastIndexOf("\\");
			String name = directory.substring(index+1);
			openFile(directory, name);
*/
		}
	}

	/**
	 * Trys to write the document as a serialization.
	 */
	class SaveAction extends AbstractAction {

		SaveAction() {
			super(saveAction);
		}

		public void actionPerformed(ActionEvent e) {

			//saveFile(getEditor().getDocument(), path, fileName);

					
			if(path ==null && fileName == null){
				Frame frame = getFrame();
				if (fileDialog == null) {
					fileDialog = new FileDialog(frame);
				}
				fileDialog.setMode(FileDialog.SAVE);
				fileDialog.show();
				fileName = fileDialog.getFile();
				if (fileName == null) {
					return;
				}
				path = fileDialog.getDirectory();
				//path = path+"/"+fileName;
			}

			//saveFile(getEditor().getDocument(), path, fileName);
		
			try {
				FileOutputStream fstrm = new FileOutputStream(new File(path, fileName), false);
				ObjectOutput ostrm = new ObjectOutputStream(fstrm);
				ostrm.writeObject(getEditor().getDocument());
				ostrm.flush();
				fstrm.close();
				ostrm.close();
				getFrame().setTitle(fileName);
			} catch (Exception e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}	

		}
	}

	/**
	 * Creates an empty document.
	 */
	class NewAction extends AbstractAction {

		NewAction() {
			super(newAction);
		}

		public void actionPerformed(ActionEvent e) {
			if(getEditor().getDocument() != null)
				getEditor().getDocument().removeUndoableEditListener
				(undoHandler);
			getEditor().setDocument(new DefaultStyledDocument());
			getEditor().getDocument().addUndoableEditListener(undoHandler);
			resetUndoManager();
			getFrame().setTitle(resources.getString("Title"));
			validate();
			
			path = null;
			fileName = null;
			paraNum = 1;
			paragraphs = new Paragraph[paraNum];
			paragraphs[0] = new Paragraph("para_1");
			paragraphs[0].isModified = true;
			getEditor().getDocument().addDocumentListener(new MyDocumentListener());
			getEditor().addCaretListener(new MyCaretListener());
		}
	}

	/**
	 * lock a chosen sub-file
	 */
	class LockAction extends AbstractAction {

		LockAction() {
			super(lockAction);
		}

		@Override
		public void actionPerformed(ActionEvent e) {
			/**
			 * according to the position of the caret, 
			 * get which paragraph this caret is in, 
			 * and then get which sub-file this user wants to lock, 
			 * invoke SmartManager.lockFile() to lock the specified file,
			 * check the return value, see whether lock is successful or not
			 */
			
			sm.lockCallBack(e);

			//System.out.println("lock a file");
		}
	
	}
	
	/**
	 * unlock a chosen sub-file
	 */
	class UnlockAction extends AbstractAction {

		UnlockAction() {
			super(unlockAction);
		}

		@Override
		public void actionPerformed(ActionEvent e) {
			/**
			 * according to the position of the caret, 
			 * get which paragraph this caret is in, 
			 * and then get which sub-file this user wants to lock, 
			 * invoke SmartManager.lockFile() to lock the specified file,
			 * check the return value, see whether lock is successful or not
			 */
			
			sm.unlockCallBack(e);
			
			//System.out.println("unlock a file");
		}
	}
	
	class DownAction extends AbstractAction {

		DownAction() {
			super(downAction);
		}

		@Override
		public void actionPerformed(ActionEvent e) {
			// TODO Auto-generated method stub
			
			sm.nextCallBack();
			
		}
		
	}
	
	class UpAction extends AbstractAction {

		UpAction() {
			super(upAction);
		}

		@Override
		public void actionPerformed(ActionEvent e) {
			// TODO Auto-generated method stub
			
			sm.previousCallBack();

		}
		
	}
	
	static class AppCloser extends WindowAdapter {
		Frame f;
		
		public AppCloser(Frame f){
			this.f = f;
		}
        public void windowClosing(WindowEvent e) {
        	
        	f.dispose();
        	sm.windowCloseCallback();
    	    System.exit(0);
    	}
    }
	
	class ColoredSquare implements Icon {
		Color color;
		public ColoredSquare(Color c) {
			this.color = c;
		}

		public void paintIcon(Component c, Graphics g, int x, int y) {
			Color oldColor = g.getColor();
			g.setColor(color);
			g.fill3DRect(x,y,getIconWidth(), getIconHeight(), true);
			g.setColor(oldColor);
		}
		public int getIconWidth() { return 12; }
		public int getIconHeight() { return 12; }

	}
	
	/**
	 * the document listener, which should be added to the current document
	 * this document listener records which paragraphs have be modified, created, or deleted
	 * 
	 * @author g0900398
	 *
	 */
	public class MyDocumentListener implements DocumentListener{

		@Override
		public void changedUpdate(DocumentEvent e) {

			sm.changeCallBack(e);
		}

		@Override
		public void insertUpdate(DocumentEvent e) {

			sm.insertCallBack(e);
		}

		@Override
		public void removeUpdate(DocumentEvent e) {

			sm.deleteCallBack(e);
		}
		
	}
	
	public class MyCaretListener implements CaretListener {

		@Override
		public void caretUpdate(CaretEvent e) {
			
			try  
            {  
				 sm.stopTimer();
                 sm.startTimer(); 
                 //System.out.println("Have started");

                 Rectangle rec = getEditor().modelToView(getEditor().getCaretPosition());
     			 int line = rec.y / rec.height + 1;
     			 
     			 // add the line of documents before the current document
     			// line += sm.countSDLinesBefore();
     			 
     		//	 int para = getEditor().getDocument().getDefaultRootElement().getElementIndex(getEditor().getCaretPosition()) + 1;
     			 
     		//	 lineLabel.setText(displayInfo + line + ", " + para);
     			lineLabel.setText(displayInfo + line);
              }  
              catch(Exception   ex)  
              {  
                   
              }   
		}
		
	}
	
	public class MyAdjustmentListener implements AdjustmentListener{

		int lastPos;
		
		public MyAdjustmentListener(int pos){
			this.lastPos = pos;
		}
		
		@Override
		public void adjustmentValueChanged(AdjustmentEvent e) {

			//System.out.println(e.getAdjustable().getValue());  
			//System.out.println(e.getAdjustable().getMaximum());
	
			if(e.getAdjustable().getValue() == 0)
				return;
			
			if(e.getAdjustable().getValue() >= 600 && e.getAdjustable().getValue() <= e.getAdjustable().getMaximum() - (684 + 3*200)){
				lastPos = e.getAdjustable().getValue();
				return;
			}
			
			if(e.getAdjustable().getValue() > e.getAdjustable().getMaximum() - (684 + 3*200) && 
					lastPos <= e.getAdjustable().getMaximum() - (684 + 3*200)){

				lastPos = e.getAdjustable().getValue();
				sm.nextCallBack();
			}
			else if(e.getAdjustable().getValue() < 600 && lastPos >= 600){
				lastPos = e.getAdjustable().getValue();
				sm.previousCallBack();
			}
		}	
	}
	/**
	 * define the inner content/run for each paragraph
	 * @author g0900398
	 *
	 */
	public static class Run {
		public Run(String styleName, String content) {
			this.styleName = styleName;
			this.content = content;
		}

		public String styleName;

		public String content;
	}

	/**
	 * define the properties for each paragraph
	 * @author g0900398
	 *
	 */
	public static class Paragraph {
		public Paragraph(String styleName, Run[] content) {
			this.styleName = styleName;
			this.content = content;
		}

		public Paragraph(String paraName){
			this.paraName = paraName;
			this.isModified = false;
			this.isLocked = false;
		}
		public float getNameInNumber(){
			return Float.parseFloat(paraName.substring(5));
		}
		public String styleName;
		public Run[] content;
		public String paraName;
		public boolean isModified = false;
		public boolean isLocked = false;
		public String locker;
	}

	/**
	 * each file is of the form para_*, where * is a float number
	 * this function sort the files based on the float number in their names
	 * @param files
	 * @return
	 */
	public File[] sortFileByName(File[] files){
		for(int i = 0; i < files.length -1; i++)
			for(int j = i+1; j < files.length; j++){
				if(Float.parseFloat((files[j].getName().substring(5))) < Float.parseFloat((files[i].getName().substring(5)))){
					File tmp = files[i];
					files[i] = files[j];
					files[j] = tmp;
				}
			}
		return files;
	}
	

	public void addMyDocumentListener(){
		
		getEditor().getDocument().addDocumentListener(myListener);
	}
	
	public void removeMyDocumentListener(){
		
		getEditor().getDocument().removeDocumentListener(myListener);
	}
	
	public void addUndoableListener(){
		
		getEditor().getDocument().addUndoableEditListener(undoHandler);
		//resetUndoManager();
	}
	
	public void removeUndoableListener(){
		
		getEditor().getDocument().removeUndoableEditListener(undoHandler);

	}

	public static void setIsLoadingNext(boolean bool){
		isLoadingNext = bool;
	}
	
	public static void setIsLoadingPrev(boolean bool){
		isLoadingPrev = bool;
	}

	public static boolean getIsLoadingNext(){
		return isLoadingNext;
	}
	
	public static boolean getIsLoadingPrev(){
		return isLoadingPrev;
	}

	public static String getUserName(){
		return userName;
	}
	
	public static Object loading = new Object();
	
}